package com.xiaoxin.experience.service.hls.common;

import com.xiaoxin.experience.util.IOUtil;
import com.xiaoxin.experience.util.IgnoredSSLSocketFactory;
import com.xiaoxin.experience.util.JSONUtil;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.HttpsURLConnection;
import java.io.*;
import java.net.HttpURLConnection;
import java.net.URL;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.OpenOption;
import java.nio.file.StandardOpenOption;
import java.util.List;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author xiaoxin
 */
@Slf4j
public class HlsDownloader
{
    private String downloadPath;

    private String fileName;

    private String saveDir;

    private int retryTimes = 3;

    private int connectTimeout = 10_000;

    private int readTimeout = 10_000;

    private String tsSaveDir;

    private boolean stop;

    private int totalTsSize;

    private ScheduledThreadPoolExecutor scheduledThreadPoolExecutor;

    private ScheduledFuture<?> schedule;

    private ConcurrentHashMap<Integer,File> tsSaveMap;

    private static final ThreadPoolExecutor THREAD_POOL_EXECUTOR;

    private Runnable onFinished = HlsDownloader::release;

    private String ffmpegPath;

    static
    {
        int threadSize = Runtime.getRuntime().availableProcessors() * 2 + 1;
        THREAD_POOL_EXECUTOR = new ThreadPoolExecutor(threadSize, threadSize,
                0, TimeUnit.MILLISECONDS,
                new LinkedBlockingDeque<>(100000));

        //设置https协议访问
        System.setProperty("https.protocols", "TLSv1.2,SSLv3");
        HttpsURLConnection.setDefaultSSLSocketFactory(IgnoredSSLSocketFactory.getSocketFactory());
        HttpsURLConnection.setDefaultHostnameVerifier((hostname,session) -> true);
    }

    public static HlsDownloader newInstance(String downloadPath)
    {
        return new HlsDownloader().downloadPath(downloadPath);
    }

    private HlsDownloader()
    {

    }

    private HlsDownloader downloadPath(String downloadPath)
    {
        this.downloadPath = downloadPath;
        return this;
    }

    public HlsDownloader fileName(String fileName)
    {
        this.fileName = fileName;
        return this;
    }

    public HlsDownloader saveDir(String saveDir)
    {
        this.saveDir = saveDir;
        return this;
    }

    public HlsDownloader ffmpeg(String ffmpegPath)
    {
        this.ffmpegPath = ffmpegPath;
        return this;
    }

    public HlsDownloader retryTimes(int retryTimes)
    {
        if (retryTimes > 0)
        {
            this.retryTimes = retryTimes;
        }
        return this;
    }

    public HlsDownloader connectTimeout(int connectTimeout)
    {
        if (connectTimeout > 0)
        {
            this.connectTimeout = connectTimeout;
        }
        return this;
    }

    public HlsDownloader readTimeout(int readTimeout)
    {
        if (readTimeout > 0)
        {
            this.readTimeout = readTimeout;
        }
        return this;
    }

    public String getDownloadPath()
    {
        return downloadPath;
    }

    public String getFileName()
    {
        return fileName;
    }

    public String getSaveDir()
    {
        return saveDir;
    }

    private boolean checkUrl()
    {
        if (downloadPath == null || !downloadPath.startsWith("http"))
        {
            return false;
        }
        try
        {
            URL url = new URL(downloadPath);
            String host = url.getHost();
            return true;
        }
        catch (Exception e)
        {
            return false;
        }
    }

    public void load(String indexFilePath)
    {
        File file = new File(indexFilePath);
        if (!file.exists())
        {
            return;
        }
        HlsParser parser = HlsParser.load(indexFilePath);
        tsSaveDir = file.getParent();
        File tsSaveDirFile = new File(tsSaveDir);
        if (!tsSaveDirFile.exists() || tsSaveDirFile.isFile())
        {
            throw new RuntimeException("ts save dir " + tsSaveDirFile.getAbsolutePath() +" not exist");
        }
        // 避免没有保存目录
        parseSaveDirByTsDir(tsSaveDirFile);
        fileNameTrimInvalidChar();
        File[] files = tsSaveDirFile.listFiles();

        tsSaveMap = new ConcurrentHashMap<>(parser.getTsDownloadPaths().size());
        if (files != null)
        {
            for (File ts : files)
            {
                if (ts.getName().endsWith(".ts"))
                {
                    tsSaveMap.put(Integer.parseInt(ts.getName().replace(".ts","")),ts);
                }
            }
        }
        downloadTss(parser);
        monitor();
    }

    private void parseSaveDirByTsDir(File tsSaveDirFile)
    {
        try
        {
            String parent = tsSaveDirFile.getParent();
            if (parent == null)
            {
                saveDir = tsSaveDirFile.getAbsolutePath();
            }
        }
        catch (Exception e)
        {
            saveDir = tsSaveDirFile.getAbsolutePath();
        }
    }

    public void download()
    {
        boolean urlIsValid = checkUrl();
        if (!urlIsValid)
        {
            log.error("download url is invalid!");
            return;
        }

        HlsParser parser = HlsParser.newInstance(downloadPath).retryTimes(retryTimes);
        try
        {
            parser.parse();
        }
        catch (Exception e)
        {
            log.error("{}:[{}] parse fail: {}",fileName,downloadPath,e.getMessage());
            return;
        }
        downloadEnvInit();
        log.debug("download env init success,file{}:[{}] start to download",fileName,downloadPath);
        totalTsSize = parser.getTsDownloadPaths().size();
        tsSaveMap = new ConcurrentHashMap<>(totalTsSize);
        saveIndexFile(parser);
        downloadTss(parser);
        monitor();
    }

    private void saveIndexFile(HlsParser parser)
    {
        File indexFile = new File(tsSaveDir,"index.m3u8");
        try(BufferedWriter bw = new BufferedWriter(new FileWriter(indexFile,false)))
        {
            bw.write(JSONUtil.toJson(parser.getHlsEncryptInfo()));
            bw.newLine();
            bw.flush();
            List<String> tsDownloadPaths = parser.getTsDownloadPaths();
            for (String tsDownloadPath : tsDownloadPaths)
            {
                bw.write(tsDownloadPath);
                bw.newLine();
                bw.flush();
            }
        }
        catch (Exception e)
        {
            log.error("save index file fail");
        }
    }

    private void downloadEnvInit()
    {
        if (saveDir == null)
        {
            saveDir = System.getProperty("user.dir");
        }
        File saveDirFile = new File(saveDir);
        if(!saveDirFile.exists() || saveDirFile.isFile())
        {
            saveDir = System.getProperty("user.dir");
        }
        fileNameTrimInvalidChar();
        File tsSaveDir = new File(saveDir,fileName);
        this.tsSaveDir = tsSaveDir.getAbsolutePath();
        if (tsSaveDir.exists())
        {
            if (tsSaveDir.isDirectory())
            {
                return;
            }
            log.error("same name file exist,create dir fail,retry change fileName to create new ts dir");
            fileName = UUID.randomUUID().toString();
            downloadEnvInit();
            return;
        }
        boolean mkdir = tsSaveDir.mkdir();
        if (!mkdir)
        {
            log.error("create ts dir fail,download[{}] stop",downloadPath);
            throw new RuntimeException("ts dir create fail!");
        }
    }

    private void downloadTss(HlsParser hlsParser)
    {
        log.debug("start to download ts");
        List<String> tsDownloadPaths = hlsParser.getTsDownloadPaths();
        AtomicInteger tsFileNameCreator = new AtomicInteger(0);
        for (String tsDownloadPath : tsDownloadPaths)
        {
            THREAD_POOL_EXECUTOR.execute(()-> downloadTs(tsFileNameCreator.getAndIncrement(),tsDownloadPath,hlsParser));
        }
    }

    private void downloadTs(int index,String tsDownloadPath,HlsParser parser)
    {
        if (stop || tsSaveMap.containsKey(index))
        {
            return;
        }
        String tsFileName = index + ".ts";
        log.debug("download {} [{}]",tsFileName,tsDownloadPath);
        File file = new File(tsSaveDir,tsFileName);
        boolean downloadResult = false;
        byte[] bytes = getBytes(tsDownloadPath);
        int times = retryTimes;
        while (times > 0)
        {
            times--;
            if (bytes != null)
            {
                break;
            }
            bytes = getBytes(tsDownloadPath);
        }
        if (bytes != null)
        {
            downloadResult = true;
            writeToFile(parser.decrypt(bytes),file,false);
            tsSaveMap.put(index,file);
        }
        log.debug("download {},result [{}]",tsFileName,downloadResult);
    }

    public void writeToFile(byte[] bytes,File destFile,boolean append)
    {
        if (Objects.isNull(bytes))
        {
            return;
        }
        OpenOption[] openOptions;
        if (append)
        {
            openOptions = new OpenOption[]{StandardOpenOption.CREATE, StandardOpenOption.APPEND, StandardOpenOption.WRITE};
        }
        else
        {
            openOptions = new OpenOption[]{StandardOpenOption.CREATE,StandardOpenOption.WRITE};
        }
        try(OutputStream fileOutputStream = Files.newOutputStream(destFile.toPath(),openOptions))
        {
            fileOutputStream.write(bytes);
            fileOutputStream.flush();
        }
        catch (IOException e)
        {
            log.error("write file fail: ",e);
        }
    }



    public void fileNameTrimInvalidChar()
    {
        if (fileName == null || fileName.isEmpty())
        {
            fileName = UUID.randomUUID().toString();
        }
        fileName = fileName.replaceAll("[^a-zA-Z0-9一-龥]", "");
    }

    public byte[] getBytes(String sourceFileUrl)
    {
        try
        {
            URL url = new URL(sourceFileUrl);
            HttpURLConnection conn = (HttpURLConnection) url.openConnection();
            //设置超时间为3秒
            conn.setConnectTimeout(connectTimeout);
            //防止屏蔽程序抓取而返回403错误
            conn.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
            //设置读取的超时时间,避免长时间读取
            conn.setReadTimeout(readTimeout);
            //得到输入流
            try(InputStream inputStream = conn.getInputStream())
            {
                //获取字节数组
                return IOUtil.readInputStream(inputStream);
            }
        }
        catch (Exception e)
        {
            return null;
        }
    }

    public void merge()
    {
        File destFile = new File(saveDir,fileName + ".mp4");
        if (destFile.exists())
        {
            boolean removeOldDest = destFile.delete();
            if (!removeOldDest)
            {
                throw new RuntimeException(destFile.getAbsolutePath() + " exist and delete fail");
            }
        }
        try(FileOutputStream fileOutputStream = new FileOutputStream(destFile,false))
        {
            FileChannel outChannel = fileOutputStream.getChannel();
            for(int i = 0; i < totalTsSize; i++)
            {
                File file = tsSaveMap.get(i);
                if (Objects.nonNull(file))
                {
                    try(FileInputStream tmpIn = new FileInputStream(file))
                    {
                        FileChannel inChannel = tmpIn.getChannel();
                        outChannel.transferFrom(inChannel,outChannel.size(),inChannel.size());
                    }
                }
            }
        }
        catch (Exception e)
        {
            log.error("merge file fail ",e);
        }
    }

    public void onFinished(Runnable runnable)
    {
        this.onFinished = runnable;
    }

    public void stop()
    {
        stop = true;
        if (scheduledThreadPoolExecutor != null)
        {
            schedule.cancel(true);
            scheduledThreadPoolExecutor.shutdownNow();
        }
    }

    public void monitor()
    {
        scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
        ScheduledFuture<?> schedule = scheduledThreadPoolExecutor.scheduleWithFixedDelay(() ->
        {
            int size = tsSaveMap.size();
            log.debug("current download ts finish process {}/{}", size, totalTsSize);
            if (totalTsSize == size)
            {
                merge();
                log.info("download {} all ts success", fileName);
                reEncode();
                onFinished.run();
                scheduledThreadPoolExecutor.shutdownNow();
            }
        }, 0, 1,TimeUnit.SECONDS);
    }

    public void reEncode()
    {
        // ffmpeg -i input.mp4 -c:v libx264 -c:a aac -strict experimental -b:a 192k output.mp4
        if (ffmpegPath == null)
        {
            return;
        }
        File sourceFile = new File(saveDir,fileName + ".mp4");
        File targetFile = new File(saveDir,fileName + "_t" + ".mp4");

        try
        {
            Process p = Runtime.getRuntime().exec(new File(ffmpegPath).getAbsolutePath() + " -i " + sourceFile.getAbsolutePath() + " -c:v libx264 -c:a aac -strict experimental -b:a 192k " + targetFile.getAbsolutePath());
            p.waitFor();
            try(BufferedReader br = new BufferedReader(new InputStreamReader(p.getErrorStream())))
            {
                br.lines().forEach(System.out::println);
            }
        }
        catch (IOException e)
        {
            // ignore
        } catch (InterruptedException e)
        {
            throw new RuntimeException(e);
        }
    }

    public static void release()
    {
        THREAD_POOL_EXECUTOR.shutdownNow();
    }
}
